Перейти к основному содержимому

5.15. Функции

Разработчику Архитектору

Функции

Функции и методы в Lua
Первоклассные объекты
Анонимные функции
Замыкания
Вариадические функции
Возврат нескольких значений

Мы уже упоминали, что функции представляют собой один из типов данных, и являтся первоклассными объектами. Это значит, что они имеют полноправный статус в системе типов - функции можно присваивать переменным, передавать как аргументы, возвращать из других функций и хранить в структурах данных.

Поэтому Lua можно относить и к языку функционального программирования.

В Lua различают функции и методы, хотя технически оба термина относятся к одному и тому же типу объекта — function. Различие носит семантический характер и связано со способом вызова и контекстом использования.

Функция — это блок кода, который может быть вызван по имени, с передачей аргументов и возвратом результата. Она определяется с помощью ключевого слова function:

function add(a, b)
return a + b
end

function — ключевое слово, сигнализирующее начало определения функции. Оно указывает интерпретатору Lua, что далее следует объявление функции, включающее имя (опционально), параметры и тело.

add — имя функции. Это идентификатор, присваиваемый функции в текущей области видимости. В данном случае add становится глобальной переменной типа function, если она не объявлена как local. Однако важно понимать, что имя — лишь ссылка на объект-функцию; сама функция может существовать и без имени (в случае анонимных функций).

(a, b) — список формальных параметров. Эти имена представляют собой локальные переменные, которые будут созданы при вызове функции и проинициализированы переданными аргументами.

a и b — формальные параметры (или просто параметры). При вызове add(5, 3), значения 5 и 3 становятся фактическими аргументами, и они присваиваются a и b соответственно.

return a + b — оператор возврата результата.

return — ключевое слово, завершающее выполнение функции и возвращающее одно или несколько значений в точку вызова.

a + b — выражение, результат которого будет возвращён. Если return отсутствует, функция неявно возвращает nil.

end — ключевое слово, обозначающее конец блока функции. Все конструкции в Lua, задающие область видимости (функции, циклы, условные операторы), закрываются этим словом.

Важно: даже если функция определена как function foo() ... end, она всё равно создаётся как объект-функция и присваивается переменной foo. Это эквивалентно:

foo = function()
...
end

Разница лишь в синтаксисе и моменте создания (в первом случае имя доступно уже во время определения, что позволяет рекурсию без дополнительных ухищрений).

Формальные параметры — имена, указанные в определении функции (a, b). Они действуют как локальные переменные внутри тела функции. Фактические аргументы — значения, переданные при вызове (5, 3).

Lua применяет передачу по значению для всех типов, но поскольку таблицы и функции являются ссылочными типами, копируется именно ссылка, а не содержимое.

Если аргументов меньше, чем параметров, недостающие получают значение nil. Если больше — лишние игнорируются (если только не используется ... — см. ниже).

Метод — это функция, привязанная к таблице (как к объекту) и предполагающая наличие неявного или явного параметра, ссылающегося на саму таблицу (обычно self). В Lua методы реализуются через специальный синтаксис ::

local obj = {
value = 42
}

function obj:print_value()
print(self.value)
end

obj:print_value() -- эквивалентно obj.print_value(obj)

Таблица в Lua — это основной составной тип, сочетающий свойства массива и ассоциативного массива (словаря). Элементы таблицы называются полями (fields) — это общепринятый термин в документации Lua. Поле может содержать любое значение, включая функцию. Когда поле содержит функцию, её принято называть методом.

local obj = {
name = "Alice", -- поле-данных
greet = function(self) -- поле-метод
print("Hello, I'm " .. self.name)
end
}

obj:greet() -- синтаксис метода

self — это не ключевое слово, а обычное имя параметра, по традиции используемое для ссылки на таблицу, из которой вызван метод. Вызов obj:greet() эквивалентен obj.greet(obj) — двоеточие автоматически подставляет obj как первый аргумент. Таким образом, : — это синтаксический сахар для вызова с передачей self.

Можно определять методы короче:

function obj:greet()
print("Hello, I'm " .. self.name)
end

Это эквивалентно предыдущему, но более читаемо.

Аналогия: self в Lua соответствует this в C++, Java, JavaScript, но с важным отличием — self должен быть явно указан как параметр. В Lua нет неявного this; он передаётся как обычный аргумент.

При использовании : первый аргумент автоматически становится self, что имитирует объектно-ориентированный стиль. Однако важно понимать, что Lua не имеет классов в традиционном смысле — вместо этого он использует прототипное наследование и метатаблицы, а методы — лишь удобный синтаксический сахар для работы с таблицами. Таким образом, метод — это функция, вызываемая с неявной передачей контекста (self), что позволяет строить абстракции, напоминающие ООП, при сохранении минимальной и гибкой модели данных.

Давайте вернёмся к «первоклассным объектам», или first-class citizens. Значит, функции:

  1. Могут быть присвоены переменным;
  2. Могут быть переданы как аргументы другим функциям;
  3. Могут быть возвращены из функций;
  4. Могут быть созданы динамически во время выполнения.

Пример:

local operation = function(a, b) return a * b end
local apply = function(f, x, y) return f(x, y) end

print(apply(operation, 5, 3)) -- 15

Здесь мы создаём функцию, и записываем её в переменную дважды. Такой подход позволяет реализовывать высокоабстрактные паттерны: каррирование, композицию функций, коллбэки, функции высшего порядка. Например, стандартные функции table.sort, string.gsub или pcall принимают функции как параметры, демонстрируя гибкость языка.

Благодаря этому, функции в Lua становятся строительными блоками не только процедурной, но и декларативной логики.

Анонимные функции (или лямбда-выражения) — это функции без имени, которые могут быть определены inline. Они особенно полезны при передаче в качестве аргументов или при создании замыканий.

Синтаксис подразумевает отсутствие имени, поэтому выглядит так:

function(...) ... end
function(x) return x^2 end

Такая функция может быть присвоена переменной, передана как аргумент, или возвращена из другой функции. У анонимной функции нет привязки имени в текущей области, но она всё равно является полноценным объектом-функцией. Их лучше использовать, когда функция нужна однократно и не требует повторного использования по имени.

Часто используются с функциями высшего порядка:

local numbers = {1, 2, 3, 4, 5}
table.sort(numbers, function(a, b) return a > b end)

Анонимные функции позволяют избежать засорения пространства имён и обеспечивают локальность определения. Поскольку они создаются динамически, каждое их вхождение порождает новый объект в памяти (хотя интерпретатор может применять оптимизации).

Понимание областей видимости критично для работы с замыканиями.

Lua использует лексические (статические) области видимости — это означает, что видимость переменной определяется местом её объявления в исходном коде, а не путём вызова.

Существуют три уровня областей: Глобальная область — переменные, объявленные без local, принадлежат глобальной таблице _G. Они доступны из любого места программы и живут до завершения выполнения.

Локальная (блочная) область — переменные, объявленные через local, ограничены блоком (функцией, циклом, условием). Их время жизни — от момента объявления до конца блока.

Функциональная область — каждая функция создаёт свою собственную область видимости. Локальные переменные функции недоступны снаружи.

local x = 10                    -- x: локальна в текущем чанке

function outer()
local y = 20 -- y: локальна в outer
function inner()
print(x, y) -- x и y доступны: лексическое окружение
end
inner()
end

Здесь inner имеет доступ к x и y, потому что была определена внутри области, где эти переменные видны.

Замыкание (closure) — это функция, захватывающая переменные из окружающей её области видимости. Это комбинация функции и ссылки на её лексическое окружение (все внешние локальные переменные, доступные в момент определения).

Ключевое свойство: замыкание сохраняет доступ к переменным, даже после завершения функции, в которой они были объявлены. Когда анонимная функция ссылается на локальную переменную из внешней функции, эта переменная продолжает существовать, даже после завершения внешней функции, до тех пор, пока замыкание активно.

function make_counter()
local count = 0
return function()
count = count + 1
return count
end
end

local c1 = make_counter()
print(c1()) -- 1
print(c1()) -- 2

Хотя make_counter() завершается, переменная count продолжает существовать, потому что замыкание (возвращаемая анонимная функция) удерживает ссылку на неё. Интерпретатор Lua автоматически управляет временем жизни таких переменных с помощью сборщика мусора.

При создании функции сохраняется ссылка на цепочку внешних локальных переменных (upvalues). Это и есть лексическое окружение. Даже если родительская функция завершена, upvalues не уничтожаются, пока на них есть ссылки.

Замыкания применяются для инкапсуляции состояния, создания приватных переменных, реализации каррирования, построения DSL, управления асинхронными операциями (через коллбэки).

Важно помнить, что замыкания увеличивают объём используемой памяти (из-за удержания ссылок), поэтому их следует использовать осознанно, особенно в долгоживущих средах.

Lua поддерживает вариадические функции — функции, принимающие произвольное количество аргументов. Это достигается с помощью специального синтаксиса ..., которое называется vararg expression.

Выражение ... внутри функции возвращает все переданные аргументы как список значений.

function log(...)
local args = {...}
for i, v in ipairs(args) do
print("Arg " .. i .. ": " .. tostring(v))
end
end

log("hello", 42, true) -- три аргумента

Внутри функции ... ведёт себя как выражение, возвращающее все переданные аргументы. Его можно распаковать, присвоить таблице или передать дальше.

Важно различать:

  • ... — выражение, возвращающее аргументы,
  • {...} — создание таблицы из всех аргументов.

Если функция объявлена как function f(a, b, ...), то a и b получат первые два аргумента, остальные — через .... ... может быть передан дальше: f(...) — удобно для декораторов и прокси.

Также ... может быть использовано при вызове других функций:

function wrapper(f, ...)
print("Calling function...")
return f(...)
end

Начиная с Lua 5.2, в строгом режиме (при наличии local _ENV) использование ... требует аккуратности, так как оно зависит от наличия vararg-контекста.

Вариадические функции широко используются в стандартной библиотеке (например, string.format, table.insert, select) и являются основой для гибких API.

Одной из уникальных черт Lua является поддержка возврата нескольких значений из функции. Это не синтаксический сахар — язык действительно позволяет функции возвращать кортеж значений, которые затем могут быть приняты в виде нескольких переменных.

Это также называют истинный множественный возврат — функция может вернуть более одного значения, разделённых запятыми.

function divide(a, b)
if b ~= 0 then
return a // b, a % b -- частное и остаток
else
return nil, "Division by zero"
end
end

local quotient, remainder = divide(10, 3)
print(quotient, remainder) -- 3 1

Выражение return x, y, z возвращает три значения. При присваивании значения распаковываются: q = 3, r = 1:

local q, r = divide(10, 3)

Если переменных больше, чем значений — лишние получают nil. Если меньше — «лишние» значения отбрасываются. Если используется в выражении: print(divide(10,3)) — печатает все значения. Lua хранит списки значений временно, в стеке. При присваивании или вызове происходит распаковка списка.

Если результатов больше, чем переменных, «лишние» значения игнорируются. Если меньше — «недостающие» переменные получают значение nil. Множественный возврат также работает при передаче в другие функции:

print(divide(10, 3))  -- напечатает: 3    1

Это поведение согласуется с концепцией списков значений (value lists) в Lua, где несколько значений могут передаваться как единый поток. Операции с ними происходят в контексте ожидаемого количества значений (например, слева от присваивания).

Стандартные функции, такие как string.find, next, unpack, активно используют этот механизм.